#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <syserr.h>
#include <device.h>
#include <utils.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include "device.h"
#include "netcore.h"
#include "printk.h"

#define to_udp_dev(dev) container_of(dev, struct udp_device, dev);

static inline int udp_could_conn(struct udp_device *udp)
{
    ip_addr_t *addr = &udp->cfg.remote_ip[udp->remote_addr_index];
    u16 port = udp->cfg.remote_port[udp->remote_addr_index];

    return !ip_addr_isany(addr) && port != 0;
}
static int udp_open(device_t *dev)
{
    int sock;
    struct udp_device *udp = to_udp_dev(dev);
    struct udp_cfg *cfg = &udp->cfg;

    DEV_PD("<%s>: new socket, dev[%s], ip[%s], port[%d]...\n",
           dev_name(dev), cfg->ethname, ipaddr_ntoa(&cfg->local_ip), cfg->local_port);

    sock = socket_new(dev, cfg->ethname, SOCK_DGRAM, &cfg->local_ip, cfg->local_port);
    if (sock < 0)
        return -ENXIO;

    ip_addr_t *addr = &cfg->remote_ip[udp->remote_addr_index];
    u16 port = cfg->remote_port[udp->remote_addr_index];

    if (udp_could_conn(udp) && net_conn(dev, addr, port) < 0)
        goto fail;

    ev__io_init(&dev->io, device_io, sock);
    ev__io_start(dev_loop(dev), &dev->io, EPOLLIN);
    dev_status(dev) = DEVICE_STATUS_CONNECTED;

    return sock;
fail:
    socket_del(sock);
    return -1;
}

static inline int is_local_changed(const struct udp_cfg *old,
                                   const struct udp_cfg *new)
{
    if ((!!old->ethname ^ !!new->ethname) != 0)
        return 1;

    if (old->ethname && new->ethname)
        if (strcmp(old->ethname, new->ethname))
            return 1;

    return !ip_addr_cmp(&old->local_ip, &new->local_ip) ||
            old->local_port != new->local_port;
}
static inline int is_remote_changed(const struct udp_cfg *old,
                                    const struct udp_cfg *new)
{
    return memcmp(old->remote_ip, new->remote_ip, sizeof(new->remote_ip)) ||
        memcmp(old->remote_port, new->remote_port, sizeof(new->remote_port));
}
static int udp_on_config_changed(struct udp_device *udp,
                                 const struct udp_cfg *new)
{
    if (!memcmp(&udp->cfg, new, sizeof(struct udp_cfg)))
        return 0;

    if (ev__is_active(&udp->dev))
    {
        if (is_local_changed(&udp->cfg, new))
            return -EIO;

        if (is_remote_changed(&udp->cfg, new))
            net_conn(&udp->dev, &new->remote_ip[0], new->remote_port[0]);
    }

    memcpy(&udp->cfg, new, sizeof(struct udp_cfg));
    return 0;
}
static int udp_ctrl(device_t *dev, u32 cmd, void *args)
{
    struct udp_device *udp = to_udp_dev(dev);

    switch (cmd)
    {
    case DEV_CTRL_CMD_GET_CFG:
        memcpy(args, &udp->cfg, sizeof(struct udp_cfg));
        break;
    case DEV_CTRL_CMD_SET_CFG:
        return udp_on_config_changed(udp, args);
    default:
        return -ENOSYS;
    }
    return 0;
}

static ssize_t udp_read(device_t *dev, void *buffer, size_t size)
{
    ssize_t ret;
    struct sockaddr_in  addr;
    socklen_t len = sizeof(addr);
    struct udp_device *udp = to_udp_dev(dev);

    if (!buffer)
    {
        while (recvfrom(dev_fd(dev), &ret, sizeof(ret), 0, NULL, NULL) > 0)
            ;
        return size;
    }

    ret = recvfrom(dev_fd(dev), buffer, size, 0, (struct sockaddr *)&addr, &len);
    if (ret > 0)
    {
        ip_addr_set_ip4_u32(&udp->peer_ip, addr.sin_addr.s_addr);
        udp->peer_port = ntohs(addr.sin_port);

        dev->dstats.rx += ret;
    }
    return ret;
}
static ssize_t udp_write(device_t *dev, const void *buffer, size_t size)
{
    struct sockaddr_in  addr;
    struct udp_device *udp = to_udp_dev(dev);
    ssize_t r;

    /* have connected before */
    if (udp_could_conn(udp))
    {
        r = write(dev_fd(dev), buffer, size);
    }
    else
    {
        if (udp->peer_port == 0)
            return -EIO;

        net_ip4_addr(&udp->peer_ip, udp->peer_port, &addr);
        r = sendto(dev_fd(dev), buffer, size, 0, (struct sockaddr *)&addr,
                   sizeof(addr));
    }
    if (r > 0)
        dev->dstats.tx += r;
    return r;
}

static const struct device_ops udp_ops =
{
    .open  = udp_open,
    .ctrl  = udp_ctrl,
    .read  = udp_read,
    .peek  = udp_read,
    .write = udp_write,
};
static const struct udp_cfg udp_server_cfg_def =
{
    .ethname       = "eth0",
    .local_ip      = IPADDR4_INIT(0),
    .remote_ip[0]  = IPADDR4_INIT(0),
    .remote_ip[1]  = IPADDR4_INIT(0),
    .local_port    = 0,
    .remote_port[0] = 0,
    .remote_port[1] = 0,
};
int udp_dev_add(const char *name, const struct udp_cfg *cfg)
{
    int ret;
    struct udp_device *udp = kmalloc(sizeof(struct udp_device));

    if (!udp)
        return -ENOMEM;

    memset(udp, 0, sizeof(struct udp_device));
    if (!cfg) cfg = &udp_server_cfg_def;
    memcpy(&udp->cfg, cfg, sizeof(struct udp_cfg));

    udp->dev.ops = &udp_ops;

    ret = device_register(&udp->dev, name, 0);
    if (ret < 0)
        kfree(udp);

    return ret;
}
void udp_dev_del(device_t *dev)
{
    struct udp_device *udp = to_udp_dev(dev);

    device_unregister(&udp->dev);
    kfree(udp);
}
